﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using CoreEx.Common.Extensions;
using CoreEx.Common.Linq;
using CoreEx.Database.Metadata;
using CoreEx.Database.Query.Expressions;
using LinFu.IoC;
using LinFu.IoC.Interfaces;

namespace CoreEx.Database.Linq.Implementation
{
    public class SimpleExpressionTranslator : ExpressionVisitor
    {
        private TranslateResult _translateResult;

        private IAliasProvider _aliasProvider;
        private ITypeInfoMapper _typeInfoMapper;

        public SimpleExpressionTranslator(IServiceContainer serviceContainer)
        {
            ServiceContainer = serviceContainer;
        }

        /// <summary>
        /// Gets the <see cref="IServiceContainer"/> that
        /// created this instance.
        /// </summary>
        protected IServiceContainer ServiceContainer { get; private set; }


        public TranslateResult Translate(Expression expression)
        {
            Visit(expression);
            return _translateResult;
        }


        /// <summary>
        /// Gets an <see cref="IAliasProvider"/> instance that is responsible 
        /// for providing table aliases.
        /// </summary>
        protected IAliasProvider AliasProvider
        {
            get { return GetAliasProvider(); }
        }

        private IAliasProvider GetAliasProvider()
        {
            if (_aliasProvider == null)
                _aliasProvider = ServiceContainer.GetService<IAliasProvider>();
            return _aliasProvider;
        }

        /// <summary>
        /// Gets the <see cref="DbExpressionFactory"/> that is used to create 
        /// new <see cref="DbExpression"/> instances.
        /// </summary>
        protected static DbExpressionFactory ExpressionFactory { get; private set; }

        /// <summary>
        /// Gets an <see cref="ITypeInfoMapper"/> instance that provides
        /// the mapping between types and database tables.
        /// </summary>
        protected ITypeInfoMapper TypeInfoMapper
        {
            get { return GetTypeInfoMapper(); }
        }

        private ITypeInfoMapper GetTypeInfoMapper()
        {
            if (_typeInfoMapper == null)
                _typeInfoMapper = ServiceContainer.GetService<ITypeInfoMapper>(string.Empty, _translateResult.ConnectionName);
            return _typeInfoMapper;
        }

        protected DbExpression CreateQuery(MemberInfo member)
        {
            TableSource tableSource = CreateTableSource(member);

            var tableExpression = tableSource.TableExpression;
            var projectionExpression = tableSource.Projection.DbExpression;

            return DbQueryFactory.Select(projectionExpression)
                .From(tableExpression);
        }


        private Projection CreateProjectionFromTableSource(TableSource tableSource)
        {
            var mappedProperties = tableSource.MappedProperties.Select(pm => pm.Value.PropertyMappingInfo);
            var projection = new Projection(tableSource);
            foreach (var mappedProperty in mappedProperties.Where(p => p.RelationType == CardinalityType.None))
            {
                var columnName = mappedProperty.ColumnInfo.ColumnName;
                var columnAlias = _translateResult.GetUniqueColumnName(columnName);
                var columnExpression = CreateAliasedColumnExpression(columnName, columnAlias, tableSource.Alias);
                var projectedColumn = new ProjectedColumn
                {
                    TargetProperty = mappedProperty.Property,
                    ColumnName = columnAlias,
                    DbExpression = columnExpression
                };
                projection.ProjectedColumns.Add(projectedColumn);
            }
            return projection;
        }


        private static DbExpression CreateAliasedColumnExpression(string columnName, string columnAlias, string columnPrefix)
        {
            var columnExpression = ExpressionFactory.Column(columnPrefix, columnName);
            if (columnAlias != columnName)
                return ExpressionFactory.Alias(columnExpression, columnAlias);
            return columnExpression;
        }


        private TableSource CreateTableSource(MemberInfo memberInfo)
        {
            var typeMappingInfo = TypeInfoMapper.MapFrom(memberInfo.GetMemberType());
            var alias = AliasProvider.GetAliasFor(memberInfo);
            var tableSource = new TableSource(typeMappingInfo)
            {
                TargetMember = memberInfo,
                Alias = alias,
                TableExpression =
                ExpressionFactory.Table(typeMappingInfo.TableInfo.TableName, alias)
            };
            Projection projection = CreateProjectionFromTableSource(tableSource);
            tableSource.Projection = projection;
            _translateResult.TableSources.Add(memberInfo, tableSource);
            return tableSource;
        }


        /// <summary>
        /// Returns the <see cref="TableSource"/> that represents a table in the final query 
        /// that is needed to navigate the target member expression.
        /// </summary>
        /// <param name="targetMemberExpression">The <see cref="MemberExpression"/> for which to return a <see cref="TableSource"/> instance.</param>
        /// <remarks>
        /// This method will create the necessary join expressions that is needed to access the database column 
        /// that the <paramref name="targetMemberExpression"/> represents.
        /// </remarks>
        /// <returns><see cref="TableSource"/></returns>
        protected TableSource GetTableSource(MemberExpression targetMemberExpression)
        {
            Expression relationalExpression = GetRelationalExpression(targetMemberExpression);

            var targetMember = GetMemberInfo(relationalExpression);

            if (IsJoined(relationalExpression))
                return _translateResult.TableSources[targetMember];

            return CreateJoinedTableSource(relationalExpression);
        }


        private Expression GetRelationalExpression(MemberExpression memberExpression)
        {
            Expression relationalExpression;
            if (MemberRepresentsRelation(memberExpression.Member))
                relationalExpression = memberExpression;
            else
                relationalExpression = memberExpression.Expression;
            return relationalExpression;
        }

        private TableSource CreateJoinedTableSource(Expression targetExpression)
        {
            var joinSequence = GetJoinSequence(targetExpression);
            foreach (var expression in joinSequence)
                EnsureMemberIsJoined(expression);
            return _translateResult.TableSources[GetMemberInfo(targetExpression)];
        }

        private bool IsJoined(Expression expression)
        {
            var memberInfo = GetMemberInfo(expression);
            if (_translateResult.TableSources.Count == 0)
                CreateQuery(memberInfo);
            return _translateResult.TableSources.ContainsKey(memberInfo);
        }

        private void EnsureMemberIsJoined(Expression targetExpression)
        {
            if (!IsJoined(targetExpression))
                CreateJoinsNeededToAccessMember((MemberExpression)targetExpression);
        }




        private void CreateJoinsNeededToAccessMember(MemberExpression memberExpression)
        {
            string alias = AliasProvider.GetAliasFor(memberExpression.Member);
            MemberInfo outerMemberInfo = GetMemberInfo(memberExpression.Expression);
            TableSource outerTableSource = _translateResult.TableSources[outerMemberInfo];

            DbExpression joinExpression = CreateJoinExpression(memberExpression, outerTableSource, alias);


            var memberInfo = memberExpression.Member;

            var typeMappingInfo = TypeInfoMapper.MapFrom(memberInfo.GetMemberType());

            var tableSource = new TableSource(typeMappingInfo);
            tableSource.Alias = alias;
            tableSource.TargetMember = memberInfo;
            tableSource.TableExpression = joinExpression;
            tableSource.OuterTableSource = outerTableSource;

            outerTableSource.JoinedTableSources.Add(tableSource);
            _translateResult.TableSources.Add(memberInfo, tableSource);
        }

        private DbExpression CreateJoinExpression(MemberExpression memberExpression, TableSource outerTableSource, string alias)
        {
            var outerAlias = outerTableSource.Alias;
            var outerPropertyMappingInfo = outerTableSource.MappedProperties[(PropertyInfo)memberExpression.Member].PropertyMappingInfo;
            var foreignKeyInfo = outerPropertyMappingInfo.ForeignKeyInfo;
            var cardinality = outerPropertyMappingInfo.RelationType;

            DbExpression joinCriteraExpression = null;
            foreach (var foreignKeyColumnInfo in foreignKeyInfo.Columns)
            {
                DbExpression equalsExpression = null;

                if (cardinality == CardinalityType.OneToOne)
                {
                    equalsExpression = ExpressionFactory.Column(outerAlias, foreignKeyColumnInfo.PrimaryColumnInfo.ColumnName) ==
                                       ExpressionFactory.Column(alias, foreignKeyColumnInfo.ForeignColumnInfo.ColumnName);
                }

                if (cardinality == CardinalityType.ManyToOne)
                {
                    equalsExpression =
                        ExpressionFactory.Column(outerAlias, foreignKeyColumnInfo.ForeignColumnInfo.ColumnName) ==
                        ExpressionFactory.Column(alias, foreignKeyColumnInfo.PrimaryColumnInfo.ColumnName);

                }

                if (joinCriteraExpression.IsNull())
                    joinCriteraExpression = equalsExpression;
                else
                    joinCriteraExpression = joinCriteraExpression & equalsExpression;


            }


            DbExpression joinExpression;
            if (cardinality == CardinalityType.OneToOne)
                joinExpression = ExpressionFactory.MakeJoin(DbJoinExpressionType.LeftOuterJoin,
                                                            ExpressionFactory.Table(foreignKeyInfo.Table.TableName, alias),
                                                            joinCriteraExpression);
            else
            {
                joinExpression = ExpressionFactory.MakeJoin(DbJoinExpressionType.LeftOuterJoin,
                                                            ExpressionFactory.Table(
                                                                foreignKeyInfo.PrimaryTable.TableName, alias),
                                                            joinCriteraExpression);
            }

            return joinExpression;
        }


        private static IEnumerable<Expression> GetJoinSequence(Expression expression)
        {
            IList<Expression> joinSequence = new List<Expression>();
            while (expression.NodeType != ExpressionType.Parameter)
            {
                joinSequence.Add(expression);
                expression = ((MemberExpression)expression).Expression;
            }
            return joinSequence.Reverse().ToList();
        }


        private static MemberInfo GetMemberInfo(Expression expression)
        {
            if (expression.NodeType == ExpressionType.MemberAccess)
                return ((MemberExpression)expression).Member;
            return expression.Type;
        }



        protected bool MemberRepresentsRelation(MemberInfo memberInfo)
        {
            var type = memberInfo.GetMemberType();
            var elementType = type.GetGenericElementType();
            return !elementType.IsSystemType() || elementType.IsEnumerable();
        }


        protected override Expression VisitMethodCall(MethodCallExpression m)
        {
            if (m.Method.Name == "Where")
            {
                
            }
            
            
            
            
            return base.VisitMethodCall(m);
        }


    }
}
